package pers.ree.demo.designpattern.singleton;

/**
 * 单例：双重检查锁模式
 */
public class DoubleCheckLockSingleton {

    private static volatile DoubleCheckLockSingleton instance;


    private DoubleCheckLockSingleton() {

    }

    public void print() {
        System.out.println("This is DoubleCheckLockSingleton instance.");
    }


    public static DoubleCheckLockSingleton getInstance() {
        //第一次判断，假设会有好多线程，如果doubleLock没有被实例化，那么就会到下一步获取锁，只有一个能获取到，
        //如果已经实例化，那么直接返回了，减少除了初始化时之外的所有锁获取等待过程
        if (instance == null) { // 1
            synchronized (DoubleCheckLockSingleton.class) {
                // 二次判断，防止并发导致两个线程都通过第一次判断的情况
                if (instance == null) { // 2
                    // 指令分解
                    // （1）为对象分配内存空间
                    // （2）初始化对象
                    // （3）对象地址被赋予变量instance
                    // instance需要加volatile，否则指令重排序可能导致(3)比（2）先执行，其他线程在1处不会进入if，直接拿到未初始化完成的对象
                    // 注意：
                    // （1）这个点有争议，有的文章认为jdk1.2之后不会出现这种情况，
                        // 文章1，https://www.iteye.com/topic/652440，
                        // 文章要点如下：
                        /*
                        确实,在JAVA2(以jdk1.2开始)以前对于实例字段是直接在主储区读写的.所以当一个线程对resource进行分配空间,
                        初始化和调用构造方法时,可能在其它线程中分配空间动作可见了,而初始化和调用构造方法还没有完成.
                        但是从JAVA2以后,JMM发生了根本的改变,分配空间,初始化,调用构造方法只会在线程的工作存储区完成,在没有
                        向主存储区复制赋值时,其它线程绝对不可能见到这个过程.而这个字段复制到主存区的过程,更不会有分配空间后
                        没有初始化或没有调用构造方法的可能.在JAVA中,一切都是按引用的值复制的.向主存储区同步其实就是把线程工作
                        存储区的这个已经构造好的对象有压缩堆地址值COPY给主存储区的那个变量.这个过程对于其它线程,要么是resource
                        为null,要么是完整的对象.绝对不会把一个已经分配空间却没有构造好的对象让其它线程可见.
                         */
                        // 文章2，https://blog.csdn.net/chenchaofuck1/article/details/51702129
                        // 要点如下：
                        /*
                        // 示例代码
                        class Singleton
                        {
                          private static Singleton instance;
                          private boolean inUse;
                          private int val;

                          private Singleton()
                          {
                            inUse = true;
                            val = 5;
                          }
                          public static Singleton getInstance()
                          {
                            if (instance == null)
                              instance = new Singleton();
                            return instance;
                          }
                        }
                        // 示例代码用Sun JDK 1.2.1 JIT 编译器生成的汇编如下：
                        ;asm code generated for getInstance
                        054D20B0   mov         eax,[049388C8]      ;load instance ref
                        054D20B5   test        eax,eax             ;test for null
                        054D20B7   jne         054D20D7
                        054D20B9   mov         eax,14C0988h
                        054D20BE   call        503EF8F0            ;allocate memory
                        054D20C3   mov         [049388C8],eax      ;store pointer in
                                                                   ;instance ref. instance
                                                                   ;non-null and ctor
                                                                   ;has not run
                        054D20C8   mov         ecx,dword ptr [eax]
                        054D20CA   mov         dword ptr [ecx],1   ;inline ctor - inUse=true;
                        054D20D0   mov         dword ptr [ecx+4],5 ;inline ctor - val=5;
                        054D20D7   mov         ebx,dword ptr ds:[49388C8h]
                        054D20DD   jmp         054D20B0

                        // 说明1
                        B0 和 B5 处的前两行汇编代码将 instance 引用从内存位置 049388C8 加载至 eax 中，并进行 null 检查。
                        这跟清单 5 中的getInstance() 方法的第一行代码相对应。第一次调用此方法时，instance 为 null，代码执行到 B9。
                        BE 处的代码为 Singleton 对象从堆中分配内存，并将一个指向该块内存的指针存储到 eax 中。
                        下一行代码，C3，获取 eax 中的指针并将其存储回内存位置为049388C8 的实例引用。
                        结果是，instance 现在为非 null 并引用一个有效的 Singleton 对象。
                        然而，此对象的构造函数尚未运行，这恰是破坏双重检查锁定的情况。然后，在 C8 行处，instance 指针被解除引用并存储到 ecx。
                        CA 和 D0 行表示内联的构造函数，该构造函数将值 true 和 5 存储到 Singleton 对象。
                        如果此代码在执行 C3 行后且在完成该构造函数前被另一个线程中断，则双重检查锁定就会失败。

                        // 说明2
                        不是所有的 JIT 编译器都生成如上代码。一些生成了代码，从而只在构造函数执行后使 instance 成为非 null。
                        针对 Java 技术的 IBM SDK 1.3 版和 Sun JDK 1.3 都生成这样的代码。
                        然而，这并不意味着应该在这些实例中使用双重检查锁定。该习语失败还有一些其他原因。
                        此外，您并不总能知道代码会在哪些 JVM 上运行，而 JIT 编译器总是会发生变化，从而生成破坏此习语的代码。
                     */
                    // （2）加volatile的方式适合jdk1.5开始的版本，之前的版本只保证可见性，不能防止指令重排
                    instance = new DoubleCheckLockSingleton();
                }
            }
        }

        return instance;
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("DoubleCheckLockSingleton finalize...");
    }
}
